package com.zk.config.api.service;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Id;
import org.apache.zookeeper.server.auth.DigestAuthenticationProvider;
import org.springframework.util.CollectionUtils;
import com.zk.config.api.client.ConfigClient;
import com.zk.config.api.client.MappingFile;
import com.zk.config.api.constants.Constants;
import com.zk.config.api.util.ComUtil;
import com.zk.config.api.util.ZkCom;
import com.zk.config.api.util.ZkOperate;
import lombok.Getter;
import lombok.Setter;

public class ZkCache {
	private static Logger logger = Logger.getLogger(ZkCache.class);
	@Getter@Setter
	private boolean watch = false;
	@Getter@Setter
	private String authInfo = "";
	@Getter@Setter
	private String log4jPathName = "log4j.properties";
	@Getter@Setter
	private String mapingToLocalTag = ":";
	@Getter@Setter
	private String mapingDir = Thread.currentThread().getContextClassLoader().getResource("").getPath();
	@Getter@Setter
	private ZooKeeper zk;
	
	public ZkCache() {
	}
	
	public ZkCache(ZooKeeper zk) {
		this.zk = zk;
	}
	
	public void init() {
		read(Constants.zkRoot, this.watch);
	}
	
	public void refresh(){
		if(zk == null) {
			return;
		}
		Map<String, byte[]> fileValue = new HashMap<>();
		Map<String, Properties> fileProperties = new HashMap<>();
		Properties properties = new Properties();
		read(fileValue, fileProperties, properties, Constants.zkRoot, this.watch);
		if (properties.size() > 0 && ConfigClient.properties != null) {
			List<Object> removekeys = new ArrayList<>();
			for(Object key:ConfigClient.properties.keySet()) {
				if(!properties.containsKey(key)) {
					removekeys.add(key);
				}
			}
			ConfigClient.properties.putAll(properties);
			for(Object key:removekeys) {
				ConfigClient.properties.remove(key);
			}
			properties.clear();
			properties = null;
			removekeys.clear();
			removekeys = null;
		} else {
			properties.clear();
			properties = null;
		}
		if (fileProperties.size() > 0) {
			Map<String, Properties> fileProperties2 = ConfigClient.fileProperties;
			ConfigClient.fileProperties = fileProperties;
			fileProperties2.clear();
			fileProperties2 = null;
		} else {
			fileProperties.clear();
			fileProperties = null;
		}
		if (fileValue.size() > 0) {
			Map<String, byte[]> fileValue2 = ConfigClient.fileValue;
			ConfigClient.fileValue = fileValue;
			fileValue2.clear();
			fileValue2 = null;
		} else {
			fileValue.clear();
			fileValue = null;
		}
	}
	
	public void reloadWatcher(String path){
		if(!this.watch) return;
		String zkPath = ZkCom.getZkPath(path);
		if (ZkOperate.existsNode(zk, zkPath, this.watch) != null) {
			List<String> childrens = ZkOperate.getChildren(zk, zkPath, this.watch);
			if (childrens != null && childrens.size() > 0) {
				zkPath = "/".equals(zkPath)?"/":(zkPath+"/");
				for(String c:childrens) {
					reloadWatcher(zkPath+c);
				}
			}
		}
	}
	
	public void read(Map<String, byte[]> fileValue, Map<String, Properties> fileProperties, Properties properties, String path, boolean watch){
		String zkPath = ZkCom.getZkPath(path);
		if (ZkOperate.existsNode(zk, zkPath, watch) != null) {
			byte[] data = ZkOperate.getNodeByte(zk, zkPath, false, null);
			List<String> childrens = ZkOperate.getChildren(zk, zkPath, watch);
			if(ComUtil.isNotEmpty(this.mapingToLocalTag) && zkPath.indexOf("/"+this.mapingToLocalTag) != -1) {
				String filePathName = zkPath.replaceAll("/"+this.mapingToLocalTag, "/");
				String filePath = this.mapingDir+filePathName;
				if(childrens == null || childrens.size() == 0) {
					if(data != null && data.length > 0) {
						Object args[] = {filePath, String.valueOf(true), data};
						MappingFile.main(args);
					} else {
					    Object args[] = {filePath, String.valueOf(false), data};
						MappingFile.main(args);
					}
				} else {
				    Object args[] = {filePath, String.valueOf(false), data};
					MappingFile.main(args);
				}
			} else {
				if (data != null && data.length > 0) {
					boolean isLog4jFile = false;
					if(ComUtil.isNotEmpty(this.log4jPathName)
							&& zkPath.endsWith(this.log4jPathName)) {
						isLog4jFile = true;
					}
					if (zkPath.endsWith(".properties") || isLog4jFile) {
						Properties p = new Properties();
						try {
							p.load(new ByteArrayInputStream(data));
						} catch (IOException e) {
							e.printStackTrace();
							logger.error("It isnt a valid properties file format.");
							return;
						}
						if(isLog4jFile) {
							PropertyConfigurator.configure(p);
						} else {
							if (properties != null) {
								properties.putAll(p);
							}
						}
						
						if(fileProperties != null) {
							fileProperties.put(zkPath, p);
						}
					} else {
						if(fileValue != null) {
							fileValue.put(zkPath, data);
						}
					}
				}
			}
			
			if (childrens != null && childrens.size() > 0) {
				zkPath = "/".equals(zkPath)?"/":(zkPath+"/");
				for(String c:childrens) {
					read(fileValue, fileProperties, properties, zkPath+c, watch);
				}
			}
		}
	}
	
	public void read(String path, boolean watch) {
		read(ConfigClient.fileValue, ConfigClient.fileProperties, ConfigClient.properties, path, watch);
	}
	
	public void reloadIfNodeDelete(String path){
		String zkPath = ZkCom.getZkPath(path);
		if(ComUtil.isNotEmpty(this.mapingToLocalTag) && zkPath.indexOf("/"+this.mapingToLocalTag) != -1) {
			
		} else {
			Map<String, byte[]> fileValue = new HashMap<>();
			Map<String, Properties> fileProperties = new HashMap<>();
			fileValue.putAll(ConfigClient.fileValue);
			String newZkPath = ZkCom.getZkEndPath(zkPath);
			for(String key:ConfigClient.fileValue.keySet()) {
				if(ZkCom.getZkEndPath(key).startsWith(newZkPath)) {
					fileValue.remove(key);
				}
			}
			if(fileValue.containsKey(zkPath)) {
				fileValue.remove(zkPath);
			}
			fileProperties.putAll(ConfigClient.fileProperties);
			for(String key:ConfigClient.fileProperties.keySet()) {
				if(ZkCom.getZkEndPath(key).startsWith(newZkPath)) {
					removeProperties(fileProperties, key);
					fileProperties.remove(key);
				}
			}
			if(fileProperties.containsKey(zkPath)) {
				removeProperties(fileProperties, zkPath);
				fileProperties.remove(zkPath);
			}
			
			Map<String, byte[]> fileValue2 = ConfigClient.fileValue;
			Map<String, Properties> fileProperties2 = ConfigClient.fileProperties;
			ConfigClient.fileValue = fileValue;
			ConfigClient.fileProperties = fileProperties;
			fileValue2.clear();
			fileValue2 = null;
			fileProperties2.clear();
			fileProperties2 = null;
		}
	}
	
	private void removeProperties(Map<String, Properties> fileProperties, String key){
		if(ConfigClient.properties == null) return;
		Properties properties = fileProperties.get(key);
		if (properties != null && properties.size() > 0) {
			for(Object k:properties.keySet()) {
				Object val = properties.get(k);
				Object val2 = ConfigClient.properties.get(k);
				if (val != null && val.equals(val2)) {
					ConfigClient.properties.remove(k);
				}
			}
		}
	}
	
	public void reloadIfNodeCreateOrChange(String path){
		String zkPath = ZkCom.getZkPath(path);
		byte[] data = ZkOperate.getNodeByte(zk, zkPath, false, null);
		if(ComUtil.isNotEmpty(this.mapingToLocalTag) && zkPath.indexOf("/"+this.mapingToLocalTag) != -1) {
			String filePathName = zkPath.replaceAll("/"+this.mapingToLocalTag, "/");
			List<String> childrens = ZkOperate.getChildren(zk, zkPath, false);
			String filePath = this.mapingDir+filePathName;
			if(childrens == null || childrens.size() == 0) {
				if(data != null && data.length > 0) {
					Object args[] = {filePath, String.valueOf(true), data};
					MappingFile.main(args);
				} else {
				    Object args[] = {filePath, String.valueOf(false), data};
					MappingFile.main(args);
				}
			} else {
			    Object args[] = {filePath, String.valueOf(false), data};
				MappingFile.main(args);
			}
		} else {
			if (data != null && data.length > 0) {
				boolean isLog4jFile = false;
				if(ComUtil.isNotEmpty(this.log4jPathName)
						&& zkPath.endsWith(this.log4jPathName)) {
					isLog4jFile = true;
				}
				if (zkPath.endsWith(".properties") || isLog4jFile) {
					Properties p = new Properties();
					try {
						p.load(new ByteArrayInputStream(data));
					} catch (IOException e) {
						e.printStackTrace();
						logger.error("It isnt a valid properties file format.");
						return;
					}
					if(isLog4jFile) {
						PropertyConfigurator.configure(p);
					} else {
						if(ConfigClient.properties != null) {
							ConfigClient.properties.putAll(p);
						}
						Properties fp = ConfigClient.fileProperties.get(zkPath);
						if(fp != null) {
							for(Object key:fp.keySet()) {
								if(!p.containsKey(key)) {
									ConfigClient.properties.remove(key);
								}
							}
						}
					}
					if(ConfigClient.fileProperties != null) {
						ConfigClient.fileProperties.put(zkPath, p);
					}
				} else {
					if(ConfigClient.fileValue != null) {
						ConfigClient.fileValue.put(zkPath, data);
					}
				}
			}
		}
	}
	
	private List<ACL> getCurrentACLList() {
		List<ACL> acls = new ArrayList<ACL>();
		try {
			Id id = new Id("digest", DigestAuthenticationProvider.generateDigest(this.authInfo));
			ACL acl = new ACL(ZooDefs.Perms.ALL, id);
			acls.add(acl);
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		return acls;
	}
	
	/**
	 * 
	 * @param ...path
	 * @param value
	 */
	public boolean addProperties(String ...pathValue) {
		boolean result = false;
		if (pathValue != null && pathValue.length > 1) {
			String content = pathValue[pathValue.length - 1];
			String key[] = new String[pathValue.length - 1];
			for(int i = 0; i < key.length; i++) {
				key[i] = pathValue[i];
			}
			String path = ZkCom.getZkPath(key);
			if(!path.endsWith(".properties")) {
				logger.error("The properties file name must end with '.properties'!");
				return result;
			}
			if(ComUtil.isNotEmpty(path) && ComUtil.isNotEmpty(content)) {
				Properties p = new Properties();
				byte[] b = content.getBytes(Constants.defualtCharset);
				try {
					p.load(new ByteArrayInputStream(b));
				} catch (IOException e) {
					e.printStackTrace();
					logger.error("It isnt a valid properties file format.");
					return result;
				}
				
				if(ComUtil.isNotEmpty(this.authInfo)) {
					result = ZkOperate.createNode(zk, path, b, getCurrentACLList());
				} else {
					result = ZkOperate.createNode(zk, path, b);
				}
				if(result) {
					ConfigClient.fileProperties.put(path, p);
					if(ConfigClient.properties != null) {
						CollectionUtils.mergePropertiesIntoMap(p, ConfigClient.properties);
					}
				}
			}
		}
		return result;
	}
	
	/**
	 * 
	 * @param ...path
	 * @param value
	 */
	public boolean updateProperties(String ...pathValue) {
		boolean result = false;
		if (pathValue != null && pathValue.length > 1) {
			String content = pathValue[pathValue.length - 1];
			String key[] = new String[pathValue.length - 1];
			for(int i = 0; i < key.length; i++) {
				key[i] = pathValue[i];
			}
			String path = ZkCom.getZkPath(key);
			if(!path.endsWith(".properties")) {
				logger.error("The properties file name must end with '.properties'!");
				return result;
			}
			if(ComUtil.isNotEmpty(path) && ComUtil.isNotEmpty(content)) {
				Properties p = new Properties();
				byte[] b = content.getBytes(Constants.defualtCharset);
				try {
					p.load(new ByteArrayInputStream(b));
				} catch (IOException e) {
					e.printStackTrace();
					logger.error("It isnt a valid properties file format.");
					return result;
				}
				if(ZkOperate.setNodeData(zk, path, b)) {
					ConfigClient.fileProperties.put(path, p);
					if(ConfigClient.properties != null) {
						removeProperties(ConfigClient.fileProperties, path);
						CollectionUtils.mergePropertiesIntoMap(p, ConfigClient.properties);
					}
					result = true;
				}
			}
		}
		return result;
	}
	
	/**
	 * 
	 * @param ...path
	 * @param value
	 */
	public boolean deleteProperties(String ...path) {
		boolean result = false;
		if (path != null && path.length > 0) {
			String keyPath = ZkCom.getZkPath(path);
			if(!keyPath.endsWith(".properties")) {
				logger.error("The properties file name must end with '.properties'!");
				return result;
			}
			if(ComUtil.isNotEmpty(keyPath)) {
				if(ZkOperate.deleteNode(zk, keyPath)) {
					removeProperties(ConfigClient.fileProperties, keyPath);
					ConfigClient.fileProperties.remove(keyPath);
					result = true;
				}
			}
		}
		return result;
	}
	
	/**
	 * 
	 * @param ...path
	 * @param value
	 */
	public boolean addFileValue(byte[] data, String ...pathValue) {
		boolean result = false;
		if (pathValue != null && pathValue.length > 1) {
			String path = ZkCom.getZkPath(pathValue);
			if(path.endsWith(".properties")) {
				logger.error("The file name can not end with '.properties'!");
				return result;
			}
			if(ComUtil.isNotEmpty(path) && data != null && data.length > 0) {
				if(ZkOperate.createNode(zk, path, data)) {
					ConfigClient.fileValue.put(path, data);
					result = true;
				}
			}
		}
		return result;
	}
	
	/**
	 * 
	 * @param ...path
	 * @param value
	 */
	public boolean updateFileValue(byte[] data, String ...pathValue) {
		boolean result = false;
		if (pathValue != null && pathValue.length > 1) {
			String path = ZkCom.getZkPath(pathValue);
			if(path.endsWith(".properties")) {
				logger.error("The file name can not end with '.properties'!");
				return result;
			}
			if(ComUtil.isNotEmpty(path) && data != null && data.length > 0) {
				if(ZkOperate.setNodeData(zk, path, data)) {
					ConfigClient.fileValue.put(path, data);
					result = true;
				}
			}
		}
		return result;
	}
	
	/**
	 * 
	 * @param ...path
	 * @param value
	 */
	public boolean deleteFileValue(String ...path) {
		boolean result = false;
		if (path != null && path.length > 0) {
			String keyPath = ZkCom.getZkPath(path);
			if(keyPath.endsWith(".properties")) {
				logger.error("The file name can not end with '.properties'!");
				return result;
			}
			if(ComUtil.isNotEmpty(keyPath)) {
				if(ZkOperate.deleteNode(zk, keyPath)) {
					ConfigClient.fileValue.remove(keyPath);
					result = true;
				}
			}
		}
		return result;
	}
}
